<?php
/**
 * micro templating engine.
 * No control blocks, only feeds variables blocks
 * Syntax is { optional part before {variable} optional part after }
 * if Datapath is available,
 * dot syntax may be used to have tree structures as objects or multilevel arrays
 *
 * This file is part of Webappkit, a packages system for PHP web apps
 * @link http://webappkit.net
 * @package webappkit
 * @subpackage utpl
 * @author J.Ducastel <nospam0@ducastel.name>
 * @license http://opensource.org/licenses/gpl-license.php GNU Public License
 */
class uTpl {

    /**
     * constructor
     * @param string $tpl template string
     */
    function __construct($source='') {
        $this->source=&$source;
    }
    function uTpl($source='') {
        $this->__construct($source);
    }

    /**
     * feeds template with data and return rendered result
     * @access public
     * @param array $data data to inject
     * @return string
     */
    function render($data=array()) {
        return $this->_feed($this->source,$data);
    }

    /***
     * get template source
     * @access public
     * @return string
     */
    function getSource() {
        return $this->source;
    }

    /***
     * set template source
     * @access public
     * @param string source
     * @return string
     */
    function setSource($source) {
        if (!is_string($source))
            return false;
        $this->source=$source;
        return true;
    }

    /**
     * feeds template with data
     * @access protected
     * @param string $tpl
     * @param array $data
     */
    function _feed($tpl,$data) {
        // parsing regular expression
        // allowing valid php names + dot for Datapath compatibility
        static $regex='\{([^{}]*)\{([a-zA-Z][a-zA-Z0-9_\.]*)\}([^{}]*)\}';
        // loop counter
        $n=0;
        while (ereg($regex,$tpl,$found)) {
            // avoiding infinite loop
            $n++; if ($n>50) break;
            // print_r($found);
            // storing optional block before var
            $before=$found[1];
            // var name
            $key=$found[2];
            // storing optional block after var
            $after=$found[3];
            // evaluating var
            $value=$this->_getKey($data,$key);
            // echo "<p>got $value for $key</p>";
            if ($this->_bool($value)) {
                // building replacement block
                $replace=$before.$this->_toString($value).$after;
            } else {
                // not provided or null value
                $replace='';
            }
            // replacing blocks
            // escaping regex special chars
            $before=str_replace('(','\(',$before);
            $before=str_replace(')','\)',$before);
            $after=str_replace('(','\(',$after);
            $after=str_replace(')','\)',$after);
            // replacing regex
            $ereg2='\{'.$before.'\{'.$key.'\}'.$after.'\}';
            $tpl=ereg_replace($ereg2,$replace,$tpl);
        }
        return $tpl;
    }

    /**
     * get key's value inside a data structure
     * use array keys or object member names
     * if not found, search for constant having the same name
     * may use Datapath
     * @access protected
     * @param array $data
     * @param string $key
     * @return mixed
     */
    function _getKey(&$data, $key) {
        if (class_exists('Datapath') and Datapath::has($data,$key)) {
            return Datapath::get($data,$key);
        } else if (is_array($data) and isset($data[$key])) {
            return $data[$key];
        } else if (is_object($data) and isset($data->$key)) {
            return $data->$key;
        } else if (defined($key)) {
            return constant($key);
        }
        return null;
    }

    /**
     * evaluates var to booleans for conditional blocks
     * @access protected
     * @param mixed $var
     * @return bool
     */
    function _bool($var) {
        return $var?true:false;
    }

    /**
     * converts vars to strings
     * @access protected
     * @param mixed var
     * @return string
     */
    function _toString($var) {
        switch (true) {
            case is_null($var):
                return $this->_nullValue();
            case is_bool($var):
                return $this->_boolValue($var);
            case is_string($var):
                return $this->_strValue($var);
            case is_object($var):
                $method='_'.get_class($var).'Value';
                if (method_exists($this,$method))
                    return $this->$method($var);
                return $this->_objectValue($var);
            default:
                return strval($var);
        }
    }

    /**
     * render value for strings
     * @access protected
     * @param string $var
     * @return string
     */
    function _strValue($var) {
        return $var;
    }

    /**
     * render value for numeric
     * @access protected
     * @param num $num
     * @return string
     */
    function _numValue($num) {
        return $num;
    }


    /**
     * render value for nulls
     * @access protected
     * @param mixed $var
     * @return string
     */
    function _nullValue() {
        return '';
    }

    /**
     * render value for booleans
     * @access protected
     * @param mixed $var
     * @return string
     */
    function _boolValue($var) {
        //return $var?'True':'False';
        return '';
    }

    /**
     *
     */

    /**
     * get value for objects
     * @access protected
     * @param mixed $var
     * @return string
     */
    function _objectValue($var) {
        return strval($var);
    }

    /**
     * @var string template source
     * @access protected
     */
    var $source;

}
